/**
 * Created by jljsj on 16/1/13.
 */

/*
* getBoundingClientRect 知识点总结
* top	Y 轴，相对于视口原点
* 判断元素是否在视窗内
* 当 DOMRect.top 小于视口高度 且 DOmRect.bottom 大于0
当 DOMRect.left 小于视口宽度 且 DOmRect.right 大于0
* todo 应当去理解一下   getBoundingClientRect 那个图形
*
* */
import React, { createElement } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import easingTypes from 'tween-functions';
import requestAnimationFrame from 'raf'; // 一个兼容性更好的，应用于动画的定时器1
import EventListener from './EventDispatcher';
import {
  noop,
  transformArguments,
  currentScrollTop,
  windowHeight,
} from './util';

let scrollLinkLists = [];

class ScrollLink extends React.Component {
  static propTypes = {
    component: PropTypes.any,
    children: PropTypes.any,
    className: PropTypes.string,
    style: PropTypes.any,
    offsetTop: PropTypes.number,
    duration: PropTypes.number,
    active: PropTypes.string,
    to: PropTypes.string,
    targetId: PropTypes.string,
    showHeightActive: PropTypes.any,
    toShowHeight: PropTypes.bool,
    ease: PropTypes.string,
    onClick: PropTypes.func,
    onFocus: PropTypes.func,
    onBlur: PropTypes.func,
    toHash: PropTypes.bool,
    componentProps: PropTypes.object,
  };
  static defaultProps = {
    component: 'div',
    offsetTop: 0,
    duration: 450, // 滚动动画持续时间
    active: 'active',
    showHeightActive: '50%',
    ease: 'easeInOutQuad',
    toHash: false,
    onClick: noop,
    onFocus: noop,
    onBlur: noop,
    componentProps: {},
  };

  constructor(props) {
    super(props);
    debugger;
    this.rafID = -1;
    this.state = {
      active: false,
    };
  }

  componentDidMount() {
    // 做的事情就是绑定事件
    this.dom = ReactDOM.findDOMNode(this);
    this.target =
      this.props.targetId && document.getElementById(this.props.targetId);
    scrollLinkLists.push(this);
    const date = Date.now();
    const length = EventListener._listeners.scroll
      ? EventListener._listeners.scroll.length
      : 0;
    this.eventType = `scroll.scrollAnchorEvent${date}${length}`;
    EventListener.addEventListener(
      this.eventType,
      this.scrollEventListener,
      this.target,
    );
    // 第一次进入；等全部渲染完成后执行;
    setTimeout(() => {
      this.scrollEventListener();
    });
  }

  componentWillUnmount() {
    scrollLinkLists = scrollLinkLists.filter(item => item !== this);
    EventListener.removeEventListener(
      this.eventType,
      this.scrollEventListener,
      this.target,
    );
    this.cancelRequestAnimationFrame();
  }

  onClick = e => {
    e.preventDefault();
    EventListener.removeAllType('scroll.scrollAnchorEvent');
    this.elementDom = document.getElementById(this.props.to);
    if (this.rafID !== -1 || !this.elementDom) {
      return;
    }
    this.scrollTop = this.target ? this.target.scrollTop : currentScrollTop();
    this.initTime = Date.now();
    // 执行滚动条动画，
    this.rafID = requestAnimationFrame(this.raf);
    // 清空所有标签
    scrollLinkLists.forEach(item => {
      if (item !== this) {
        item.remActive();
      }
    });
    // 添加 active
    this.addActive();
  };

  getToTop = () => {
    const elementRect =
      this.elementDom && this.elementDom.getBoundingClientRect();
    this.clientHeight = this.target ? this.target.clientHeight : windowHeight();
    const targetTop = this.target ? this.target.getBoundingClientRect().top : 0;
    const toTop =
      Math.round(elementRect.top + currentScrollTop()) -
      this.props.offsetTop -
      targetTop;
    const t = transformArguments(this.props.showHeightActive)[0];
    const toShow = t.match('%') ? (this.clientHeight * parseFloat(t)) / 100 : t;
    return this.props.toShowHeight ? toTop - toShow + 0.5 : toTop;
  };

  cancelRequestAnimationFrame = () => {
    requestAnimationFrame.cancel(this.rafID);
    this.rafID = -1;
  };

  addActive = () => {
    if (!this.state.active) {
      const obj = {
        target: this.dom,
        to: this.props.to,
      };
      this.props.onFocus(obj);
      this.setState(
        {
          active: true,
        },
        () => {
          if (this.props.toHash) {
            const link = `#${this.props.to}`;
            history.pushState(null, window.title, link); // eslint-disable-line
          }
        },
      );
    }
  };

  raf = () => {
    if (this.rafID === -1) {
      return;
    }
    const duration = this.props.duration;
    const now = Date.now();
    // 表示过了多少时间了
    const progressTime =
      now - this.initTime > duration ? duration : now - this.initTime;
    // 动画时也会改变高度，动态获取
    // 不得不说，这个 easingTypes 是一个和 requestAnimationFrame 搭配使用，很有用的一个库
    const easeValue = easingTypes[this.props.ease](
      progressTime,
      this.scrollTop,
      this.getToTop(),
      duration,
    );

    if (this.target) {
      this.target.scrollTop = easeValue;
    } else {
      window.scrollTo(window.scrollX, easeValue);
    }
    // 递归退出条件
    if (progressTime === duration) {
      this.elementDom = null;
      this.cancelRequestAnimationFrame();
      EventListener.reAllType('scroll.scrollAnchorEvent');
    } else {
      this.rafID = requestAnimationFrame(this.raf);
    }
  };

  remActive = () => {
    if (this.state.active) {
      const obj = {
        target: this.dom,
        to: this.props.to,
      };
      this.props.onBlur(obj);
      this.setState({
        active: false,
      });
    }
  };

  scrollEventListener = () => {
    /**
     * @核心 在于  通过 getBoundingClientRect api 判断，元素是否在视口中
     * 并且距离顶部和出顶部多少，都可以根据用户的需求镜像配置
     * */
    const elementDom = document.getElementById(this.props.to);
    if (!elementDom) {
      return;
    }
    // 滚动时会改变高度, 动态获取高度
    const clientHeight = this.target
      ? this.target.clientHeight
      : windowHeight();
    const elementRect = elementDom.getBoundingClientRect();
    const elementClientHeight = elementDom.clientHeight;
    const targetTop = this.target ? this.target.getBoundingClientRect().top : 0;
    const top = Math.round(-elementRect.top + targetTop); // 变为负的，我真的很不认可，会为后面看代码的造成很大曲折
    const showHeightActive = transformArguments(this.props.showHeightActive); // 支持多种写法
    const startShowHeight =
      showHeightActive[0].toString().indexOf('%') >= 0
        ? (parseFloat(showHeightActive[0]) / 100) * clientHeight
        : parseFloat(showHeightActive[0]);
    const endShowHeight =
      showHeightActive[1].toString().indexOf('%') >= 0
        ? (parseFloat(showHeightActive[1]) / 100) * clientHeight
        : parseFloat(showHeightActive[1]);
    if (
      top >= Math.round(-startShowHeight) &&
      top < Math.round(elementClientHeight - endShowHeight)
    ) {
      this.addActive();
    } else {
      this.remActive();
    }
  };

  render() {
    const {
      component,
      onClick,
      duration,
      active: tagActive,
      showHeightActive,
      ease,
      toShowHeight,
      offsetTop,
      targetId,
      to,
      toHash,
      componentProps,
      ...props
    } = this.props;
    const active = this.state.active ? tagActive : '';
    props.onClick = e => {
      onClick(e);
      this.onClick(e);
    };
    const reg = new RegExp(active, 'ig');
    const className = props.className || '';
    props.className =
      className.indexOf(active) === -1
        ? `${className} ${active}`.trim()
        : className.replace(reg, '').trim();
    return createElement(this.props.component, { ...props, ...componentProps });
  }
}
ScrollLink.isScrollLink = true;

export default ScrollLink;
